// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// File:	projectscheduler.cpp
// Author: 	Kirk Larkin
// Date: 	25/09/2008
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#include "projectscheduler.h"

// Qt Includes
#include <QFile>
#include <QXmlStreamReader>
#include <QtDebug>

// Local Includes
#include "scheduledproject.h"

// Constants
const quint16 gc_nTimerInterval = 1000;

// ------------------------------------------------------------------
// Construction & Destruction
// ------------------------------------------------------------------
ProjectScheduler::ProjectScheduler(const QString &strConfigFilePath/* = QString()*/,
		QObject *pParent/* = NULL*/)
	: QObject(pParent),
	  m_pCurrentProject(NULL)
{
	// Load the projects from file, if configured so.
	if(!strConfigFilePath.isEmpty())
		loadConfigFile(strConfigFilePath);

	// Start the timer for "ticking" the scheduler.
	startTimer(gc_nTimerInterval);
}

// ------------------------------------------------------------------
// Member Functions
// ------------------------------------------------------------------
bool ProjectScheduler::addProject(const QString &strProjectPath, const QTime &tmProjectSchedule)
{
		// The project must be an ini file.
	if(!strProjectPath.endsWith(tr(".ini")))
		return false;

	// The project file must exist.
	if(!QFile::exists(strProjectPath))
		return false;

	ScheduledProject *const pScheduledProject = new ScheduledProject(strProjectPath, tmProjectSchedule);

	// Add the scheduled project to our collection, which is an unordered set of projects
	// to schedule.
	m_lstProjects.append(pScheduledProject);

	// And to our queue, as we want to run the project as soon as possible once it has been added.
	m_quProjects.enqueue(pScheduledProject);
	
	logMessage(QString(tr("Added project: %1 for scheduling.")).arg(strProjectPath));

	return true;
}

bool ProjectScheduler::loadConfigFile(const QString &strConfigFilePath)
{
	// Check the config file is valid.
	if(!strConfigFilePath.endsWith(tr(".xml")) || !QFile::exists(strConfigFilePath))
		return false;

	QFile configFile(strConfigFilePath);

	if(!configFile.open(QIODevice::ReadOnly | QIODevice::Text))
		return false;

	logMessage(QString(tr("Loading schedule config from: %1.")).arg(strConfigFilePath));

	QString strProjectPath;
	QTime tmProjectSchedule;
	QXmlStreamReader xmlStreamReader(&configFile);

	while(!xmlStreamReader.atEnd())
	{
		QXmlStreamReader::TokenType eTokenType = xmlStreamReader.readNext();

		switch(eTokenType)
		{
			case QXmlStreamReader::StartElement:
			{
				const QString strElementName = xmlStreamReader.name().toString();

				if(strElementName == QObject::tr("project"))
					strProjectPath = xmlStreamReader.attributes().value(QObject::tr("path")).toString();
				else if(strElementName == QObject::tr("trigger"))
				{
					const QString strProjectSchedule = xmlStreamReader.attributes().value(QObject::tr("period")).toString();

					if(!strProjectSchedule.isEmpty())
						tmProjectSchedule = QTime::fromString(strProjectSchedule, QObject::tr("hh:mm:ss"));
				}
				else if(strElementName == QObject::tr("log"))
					m_strLogPath = xmlStreamReader.attributes().value(QObject::tr("path")).toString();

				break;
			}

			case QXmlStreamReader::EndElement:
			{
				const QString strElementName = xmlStreamReader.name().toString();

				if(strElementName == QObject::tr("project"))
				{
					if(!strProjectPath.isEmpty() && !tmProjectSchedule.isNull())
						addProject(strProjectPath, tmProjectSchedule);

					strProjectPath.clear();
					tmProjectSchedule = QTime();
				}
			}

			default:
				break;
		}
	}

	configFile.close();

	return true;
}

bool ProjectScheduler::startNextProject()
{
	// We expect the caller of this function to ensure there is a project to be dequeued.
	ScheduledProject *const pScheduledProject = m_quProjects.dequeue();

	// Sanity check.
	if(!pScheduledProject)
		return false;

	m_pCurrentProject = pScheduledProject;

	// Signals & Slots.
	connect(m_pCurrentProject, SIGNAL(projectComplete(Project::tProjectState)), this,
				SLOT(onCurrentProjectComplete(Project::tProjectState)));

	// Start the project.
	m_pCurrentProject->runProject();
	
	logMessage(QString(tr("Running scheduled project: %1...")).arg(pScheduledProject->getProjectPath()));

	return true;
}

void ProjectScheduler::logMessage(const QString &strLogMessage) const
{
	if(m_strLogPath.isEmpty())
		return;
	
	QFile logFile(m_strLogPath);
	
	if(!logFile.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text))
		return;
	
	QTextStream fileStream(&logFile);
	const QString strLogOut = QDateTime::currentDateTime().toString(QObject::tr("dd/MM/yyyy hh:mm:ss - %1")).arg(strLogMessage);
	
	fileStream << strLogOut << "\n";
	fileStream.flush();
			
	logFile.close();
	
	QTextStream outStream(stdout);
	outStream << strLogOut << endl;
}

// ------------------------------------------------------------------
// Overrides
// ------------------------------------------------------------------
void ProjectScheduler::timerEvent(QTimerEvent *pEvent)
{
	Q_UNUSED(pEvent);
	
	// If we have an active project there's no work for the scheduler.
	if(m_pCurrentProject)
		return;

	// Update the queue accordingly.
	const QDateTime dtCurrent = QDateTime::currentDateTime();
	QList<ScheduledProject*>::const_iterator itProjects;

	for(itProjects = m_lstProjects.constBegin(); itProjects != m_lstProjects.constEnd(); itProjects++)
	{
		ScheduledProject *const pScheduledProject = *itProjects;

		// Sanity check.
		if(!pScheduledProject)
			continue;

		const QDateTime &dtNextRun = pScheduledProject->getNextRunDateTime();

		// If the time is invalid it means nothing to us here.
		if(dtNextRun.isNull())
			continue;

		// Is it time to queue the project?
		if(dtCurrent >= dtNextRun)
		{
			// Yes, but is the project already queued?
			// TODO: Flag that the project has been scheduled (inside map, inside ScheduledProject)?
			if(!m_quProjects.contains(pScheduledProject))
				m_quProjects.enqueue(pScheduledProject);
		}
	}

	// Can we start a new project?
	if(m_quProjects.count() == 0)
		return;

	startNextProject();
}

// ------------------------------------------------------------------
// Private Slots
// ------------------------------------------------------------------
void ProjectScheduler::onCurrentProjectComplete(Project::tProjectState eProjectState)
{
	Q_UNUSED(eProjectState);
	
	disconnect(m_pCurrentProject, SIGNAL(projectComplete(Project::tProjectState)), this,
			SLOT(onCurrentProjectComplete(Project::tProjectState)));

	logMessage(QString(tr("Scheduled project complete: %1.")).arg(m_pCurrentProject->getProjectPath()));
	m_pCurrentProject = NULL;
}
